{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# An Introduction to SageMaker LDA\n",
    "\n",
    "***Finding topics in synthetic document data using Spectral LDA algorithms.***\n",
    "\n",
    "---\n",
    "\n",
    "1. [Introduction](#Introduction)\n",
    "1. [Setup](#Setup)\n",
    "1. [Training](#Training)\n",
    "1. [Inference](#Inference)\n",
    "1. [Epilogue](#Epilogue)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Introduction\n",
    "***\n",
    "\n",
    "Amazon SageMaker LDA is an unsupervised learning algorithm that attempts to describe a set of observations as a mixture of distinct categories. Latent Dirichlet Allocation (LDA) is most commonly used to discover a user-specified number of topics shared by documents within a text corpus. Here each observation is a document, the features are the presence (or occurrence count) of each word, and the categories are the topics. Since the method is unsupervised, the topics are not specified up front, and are not guaranteed to align with how a human may naturally categorize documents. The topics are learned as a probability distribution over the words that occur in each document. Each document, in turn, is described as a mixture of topics.\n",
    "\n",
    "In this notebook we will use the Amazon SageMaker LDA algorithm to train an LDA model on some example synthetic data. We will then use this model to classify (perform inference on) the data. The main goals of this notebook are to,\n",
    "\n",
    "* learn how to obtain and store data for use in Amazon SageMaker,\n",
    "* create an AWS SageMaker training job on a data set to produce an LDA model,\n",
    "* use the LDA model to perform inference with an Amazon SageMaker endpoint.\n",
    "\n",
    "The following are ***not*** goals of this notebook:\n",
    "\n",
    "* understand the LDA model,\n",
    "* understand how the Amazon SageMaker LDA algorithm works,\n",
    "* interpret the meaning of the inference output\n",
    "\n",
    "If you would like to know more about these things take a minute to run this notebook and then check out the SageMaker LDA Documentation and the **LDA-Science.ipynb** notebook."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "!conda install -y scipy"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "%matplotlib inline\n",
    "\n",
    "import os, re\n",
    "\n",
    "import boto3\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "np.set_printoptions(precision=3, suppress=True)\n",
    "\n",
    "# some helpful utility functions are defined in the Python module\n",
    "# \"generate_example_data\" located in the same directory as this\n",
    "# notebook\n",
    "from generate_example_data import generate_griffiths_data, plot_lda, match_estimated_topics\n",
    "\n",
    "# accessing the SageMaker Python SDK\n",
    "import sagemaker\n",
    "from sagemaker.amazon.common import numpy_to_record_serializer\n",
    "from sagemaker.predictor import csv_serializer, json_deserializer"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Setup\n",
    "\n",
    "***\n",
    "\n",
    "*This notebook was created and tested on an ml.m4.xlarge notebook instance.*\n",
    "\n",
    "Before we do anything at all, we need data! We also need to setup our AWS credentials so that AWS SageMaker can store and access data. In this section we will do four things:\n",
    "\n",
    "1. [Setup AWS Credentials](#SetupAWSCredentials)\n",
    "1. [Obtain Example Dataset](#ObtainExampleDataset)\n",
    "1. [Inspect Example Data](#InspectExampleData)\n",
    "1. [Store Data on S3](#StoreDataonS3)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Setup AWS Credentials\n",
    "\n",
    "We first need to specify some AWS credentials; specifically data locations and access roles. This is the only cell of this notebook that you will need to edit. In particular, we need the following data:\n",
    "\n",
    "* `bucket` - An S3 bucket accessible by this account.\n",
    "  * Used to store input training data and model data output.\n",
    "  * Should be withing the same region as this notebook instance, training, and hosting.\n",
    "* `prefix` - The location in the bucket where this notebook's input and and output data will be stored. (The default value is sufficient.)\n",
    "* `role` - The IAM Role ARN used to give training and hosting access to your data.\n",
    "  * See documentation on how to create these.\n",
    "  * The script below will try to determine an appropriate Role ARN."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true,
    "isConfigCell": true
   },
   "outputs": [],
   "source": [
    "from sagemaker import get_execution_role\n",
    "\n",
    "role = get_execution_role()\n",
    "bucket = '<your_s3_bucket_name_here>'\n",
    "prefix = 'sagemaker/DEMO-lda-introduction'\n",
    "\n",
    "print('Training input/output will be stored in {}/{}'.format(bucket, prefix))\n",
    "print('\\nIAM Role: {}'.format(role))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Obtain Example Data\n",
    "\n",
    "\n",
    "We generate some example synthetic document data. For the purposes of this notebook we will omit the details of this process. All we need to know is that each piece of data, commonly called a *\"document\"*, is a vector of integers representing *\"word counts\"* within the document. In this particular example there are a total of 25 words in the *\"vocabulary\"*.\n",
    "\n",
    "$$\n",
    "\\underbrace{w}_{\\text{document}} = \\overbrace{\\big[ w_1, w_2, \\ldots, w_V \\big] }^{\\text{word counts}},\n",
    "\\quad\n",
    "V = \\text{vocabulary size}\n",
    "$$\n",
    "\n",
    "These data are based on that used by Griffiths and Steyvers in their paper [Finding Scientific Topics](http://psiexp.ss.uci.edu/research/papers/sciencetopics.pdf). For more information, see the **LDA-Science.ipynb** notebook."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "print('Generating example data...')\n",
    "num_documents = 6000\n",
    "num_topics = 5\n",
    "known_alpha, known_beta, documents, topic_mixtures = generate_griffiths_data(\n",
    "    num_documents=num_documents, num_topics=num_topics)\n",
    "vocabulary_size = len(documents[0])\n",
    "\n",
    "# separate the generated data into training and tests subsets\n",
    "num_documents_training = int(0.9*num_documents)\n",
    "num_documents_test = num_documents - num_documents_training\n",
    "\n",
    "documents_training = documents[:num_documents_training]\n",
    "documents_test = documents[num_documents_training:]\n",
    "\n",
    "topic_mixtures_training = topic_mixtures[:num_documents_training]\n",
    "topic_mixtures_test = topic_mixtures[num_documents_training:]\n",
    "\n",
    "print('documents_training.shape = {}'.format(documents_training.shape))\n",
    "print('documents_test.shape = {}'.format(documents_test.shape))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Inspect Example Data\n",
    "\n",
    "*What does the example data actually look like?* Below we print an example document as well as its corresponding known *topic-mixture*. A topic-mixture serves as the \"label\" in the LDA model. It describes the ratio of topics from which the words in the document are found.\n",
    "\n",
    "For example, if the topic mixture of an input document $\\mathbf{w}$ is,\n",
    "\n",
    "$$\\theta = \\left[ 0.3, 0.2, 0, 0.5, 0 \\right]$$\n",
    "\n",
    "then $\\mathbf{w}$ is 30% generated from the first topic, 20% from the second topic, and 50% from the fourth topic. For more information see **How LDA Works** in the SageMaker documentation as well as the **LDA-Science.ipynb** notebook.\n",
    "\n",
    "Below, we compute the topic mixtures for the first few training documents. As we can see, each document is a vector of word counts from the 25-word vocabulary and its topic-mixture is a probability distribution across the five topics used to generate the sample dataset."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "print('First training document =\\n{}'.format(documents[0]))\n",
    "print('\\nVocabulary size = {}'.format(vocabulary_size))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "print('Known topic mixture of first document =\\n{}'.format(topic_mixtures_training[0]))\n",
    "print('\\nNumber of topics = {}'.format(num_topics))\n",
    "print('Sum of elements = {}'.format(topic_mixtures_training[0].sum()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Later, when we perform inference on the training data set we will compare the inferred topic mixture to this known one.\n",
    "\n",
    "---\n",
    "\n",
    "Human beings are visual creatures, so it might be helpful to come up with a visual representation of these documents. In the below plots, each pixel of a document represents a word. The greyscale intensity is a measure of how frequently that word occurs. Below we plot the first few documents of the training set reshaped into 5x5 pixel grids."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "%matplotlib inline\n",
    "\n",
    "fig = plot_lda(documents_training, nrows=3, ncols=4, cmap='gray_r', with_colorbar=True)\n",
    "fig.suptitle('Example Document Word Counts')\n",
    "fig.set_dpi(160)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Store Data on S3\n",
    "\n",
    "A SageMaker training job needs access to training data stored in an S3 bucket. Although training can accept data of various formats we convert the documents MXNet RecordIO Protobuf format before uploading to the S3 bucket defined at the beginning of this notebook. We do so by making use of the SageMaker Python SDK utility `numpy_to_record_serializer`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "# convert documents_training to Protobuf RecordIO format\n",
    "recordio_protobuf_serializer = numpy_to_record_serializer()\n",
    "fbuffer = recordio_protobuf_serializer(documents_training)\n",
    "\n",
    "# upload to S3 in bucket/prefix/train\n",
    "fname = 'lda.data'\n",
    "s3_object = os.path.join(prefix, 'train', fname)\n",
    "boto3.Session().resource('s3').Bucket(bucket).Object(s3_object).upload_fileobj(fbuffer)\n",
    "\n",
    "s3_train_data = 's3://{}/{}'.format(bucket, s3_object)\n",
    "print('Uploaded data to S3: {}'.format(s3_train_data))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Training\n",
    "\n",
    "***\n",
    "\n",
    "Once the data is preprocessed and available in a recommended format the next step is to train our model on the data. There are number of parameters required by SageMaker LDA configuring the model and defining the computational environment in which training will take place.\n",
    "\n",
    "First, we specify a Docker container containing the SageMaker LDA algorithm. For your convenience, a region-specific container is automatically chosen for you to minimize cross-region data communication. Information about the locations of each SageMaker algorithm is available in the documentation."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "from sagemaker.amazon.amazon_estimator import get_image_uri\n",
    "# select the algorithm container based on this notebook's current location\n",
    "\n",
    "region_name = boto3.Session().region_name\n",
    "container = get_image_uri(region_name, 'lda')\n",
    "\n",
    "print('Using SageMaker LDA container: {} ({})'.format(container, region_name))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Particular to a SageMaker LDA training job are the following hyperparameters:\n",
    "\n",
    "* **`num_topics`** - The number of topics or categories in the LDA model.\n",
    "  * Usually, this is not known a priori.\n",
    "  * In this example, howevever, we know that the data is generated by five topics.\n",
    "\n",
    "* **`feature_dim`** - The size of the *\"vocabulary\"*, in LDA parlance.\n",
    "  * In this example, this is equal 25.\n",
    "\n",
    "* **`mini_batch_size`** - The number of input training documents.\n",
    "\n",
    "* **`alpha0`** - *(optional)* a measurement of how \"mixed\" are the topic-mixtures.\n",
    "  * When `alpha0` is small the data tends to be represented by one or few topics.\n",
    "  * When `alpha0` is large the data tends to be an even combination of several or many topics.\n",
    "  * The default value is `alpha0 = 1.0`.\n",
    "\n",
    "In addition to these LDA model hyperparameters, we provide additional parameters defining things like the EC2 instance type on which training will run, the S3 bucket containing the data, and the AWS access role. Note that,\n",
    "\n",
    "* Recommended instance type: `ml.c4`\n",
    "* Current limitations:\n",
    "  * SageMaker LDA *training* can only run on a single instance.\n",
    "  * SageMaker LDA does not take advantage of GPU hardware.\n",
    "  * (The Amazon AI Algorithms team is working hard to provide these capabilities in a future release!)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "session = sagemaker.Session()\n",
    "\n",
    "# specify general training job information\n",
    "lda = sagemaker.estimator.Estimator(\n",
    "    container,\n",
    "    role,\n",
    "    output_path='s3://{}/{}/output'.format(bucket, prefix),\n",
    "    train_instance_count=1,\n",
    "    train_instance_type='ml.c4.2xlarge',\n",
    "    sagemaker_session=session,\n",
    ")\n",
    "\n",
    "# set algorithm-specific hyperparameters\n",
    "lda.set_hyperparameters(\n",
    "    num_topics=num_topics,\n",
    "    feature_dim=vocabulary_size,\n",
    "    mini_batch_size=num_documents_training,\n",
    "    alpha0=1.0,\n",
    ")\n",
    "\n",
    "# run the training job on input data stored in S3\n",
    "lda.fit({'train': s3_train_data})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If you see the message\n",
    "\n",
    "> `===== Job Complete =====`\n",
    "\n",
    "at the bottom of the output logs then that means training sucessfully completed and the output LDA model was stored in the specified output path. You can also view information about and the status of a training job using the AWS SageMaker console. Just click on the \"Jobs\" tab and select training job matching the training job name, below:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "print('Training job name: {}'.format(lda.latest_training_job.job_name))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Inference\n",
    "\n",
    "***\n",
    "\n",
    "A trained model does nothing on its own. We now want to use the model we computed to perform inference on data. For this example, that means predicting the topic mixture representing a given document.\n",
    "\n",
    "We create an inference endpoint using the SageMaker Python SDK `deploy()` function from the job we defined above. We specify the instance type where inference is computed as well as an initial number of instances to spin up."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "lda_inference = lda.deploy(\n",
    "    initial_instance_count=1,\n",
    "    instance_type='ml.m4.xlarge',  # LDA inference may work better at scale on ml.c4 instances\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "Congratulations! You now have a functioning SageMaker LDA inference endpoint. You can confirm the endpoint configuration and status by navigating to the \"Endpoints\" tab in the AWS SageMaker console and selecting the endpoint matching the endpoint name, below: "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "print('Endpoint name: {}'.format(lda_inference.endpoint))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "With this realtime endpoint at our fingertips we can finally perform inference on our training and test data.\n",
    "\n",
    "We can pass a variety of data formats to our inference endpoint. In this example we will demonstrate passing CSV-formatted data. Other available formats are JSON-formatted, JSON-sparse-formatter, and RecordIO Protobuf. We make use of the SageMaker Python SDK utilities `csv_serializer` and `json_deserializer` when configuring the inference endpoint."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "lda_inference.content_type = 'text/csv'\n",
    "lda_inference.serializer = csv_serializer\n",
    "lda_inference.deserializer = json_deserializer"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We pass some test documents to the inference endpoint. Note that the serializer and deserializer will atuomatically take care of the datatype conversion from Numpy NDArrays."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "results = lda_inference.predict(documents_test[:12])\n",
    "\n",
    "print(results)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "It may be hard to see but the output format of SageMaker LDA inference endpoint is a Python dictionary with the following format.\n",
    "\n",
    "```\n",
    "{\n",
    "  'predictions': [\n",
    "    {'topic_mixture': [ ... ] },\n",
    "    {'topic_mixture': [ ... ] },\n",
    "    {'topic_mixture': [ ... ] },\n",
    "    ...\n",
    "  ]\n",
    "}\n",
    "```\n",
    "\n",
    "We extract the topic mixtures, themselves, corresponding to each of the input documents."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "computed_topic_mixtures = np.array([prediction['topic_mixture'] for prediction in results['predictions']])\n",
    "\n",
    "print(computed_topic_mixtures)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If you decide to compare these results to the known topic mixtures generated in the [Obtain Example Data](#ObtainExampleData) Section keep in mind that SageMaker LDA discovers topics in no particular order. That is, the approximate topic mixtures computed above may be permutations of the known topic mixtures corresponding to the same documents."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "print(topic_mixtures_test[0])      # known test topic mixture\n",
    "print(computed_topic_mixtures[0])  # computed topic mixture (topics permuted)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Stop / Close the Endpoint\n",
    "\n",
    "Finally, we should delete the endpoint before we close the notebook.\n",
    "\n",
    "To do so execute the cell below. Alternately, you can navigate to the \"Endpoints\" tab in the SageMaker console, select the endpoint with the name stored in the variable `endpoint_name`, and select \"Delete\" from the \"Actions\" dropdown menu. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "sagemaker.Session().delete_endpoint(lda_inference.endpoint)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Epilogue\n",
    "\n",
    "---\n",
    "\n",
    "In this notebook we,\n",
    "\n",
    "* generated some example LDA documents and their corresponding topic-mixtures,\n",
    "* trained a SageMaker LDA model on a training set of documents,\n",
    "* created an inference endpoint,\n",
    "* used the endpoint to infer the topic mixtures of a test input.\n",
    "\n",
    "There are several things to keep in mind when applying SageMaker LDA to real-word data such as a corpus of text documents. Note that input documents to the algorithm, both in training and inference, need to be vectors of integers representing word counts. Each index corresponds to a word in the corpus vocabulary. Therefore, one will need to \"tokenize\" their corpus vocabulary.\n",
    "\n",
    "$$\n",
    "\\text{\"cat\"} \\mapsto 0, \\; \\text{\"dog\"} \\mapsto 1 \\; \\text{\"bird\"} \\mapsto 2, \\ldots\n",
    "$$\n",
    "\n",
    "Each text document then needs to be converted to a \"bag-of-words\" format document.\n",
    "\n",
    "$$\n",
    "w = \\text{\"cat bird bird bird cat\"} \\quad \\longmapsto \\quad w = [2, 0, 3, 0, \\ldots, 0]\n",
    "$$\n",
    "\n",
    "Also note that many real-word applications have large vocabulary sizes. It may be necessary to represent the input documents in sparse format. Finally, the use of stemming and lemmatization in data preprocessing provides several benefits. Doing so can improve training and inference compute time since it reduces the effective vocabulary size. More importantly, though, it can improve the quality of learned topic-word probability matrices and inferred topic mixtures. For example, the words *\"parliament\"*, *\"parliaments\"*, *\"parliamentary\"*, *\"parliament's\"*, and *\"parliamentarians\"* are all essentially the same word, *\"parliament\"*, but with different conjugations. For the purposes of detecting topics, such as a *\"politics\"* or *governments\"* topic, the inclusion of all five does not add much additional value as they all essentiall describe the same feature."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "conda_mxnet_p36",
   "language": "python",
   "name": "conda_mxnet_p36"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.3"
  },
  "notice": "Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.  Licensed under the Apache License, Version 2.0 (the \"License\"). You may not use this file except in compliance with the License. A copy of the License is located at http://aws.amazon.com/apache2.0/ or in the \"license\" file accompanying this file. This file is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License."
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
